package cmd

import (
	"context"
	"fmt"
	"os"
	"os/exec"

	"github.com/opentracing/opentracing-go"
	"github.com/opentracing/opentracing-go/ext"
	"github.com/spf13/cobra"
	"github.com/uber/jaeger-client-go"
	"go.uber.org/zap/zapcore"

	"golang.aliyun-inc.com/agit/agit/proc-receive-hook/config"
	"golang.aliyun-inc.com/agit/agit/proc-receive-hook/hook/alibaba"
	"golang.aliyun-inc.com/agit/agit/proc-receive-hook/internal/tracing"
	"golang.aliyun-inc.com/agit/agit/proc-receive-hook/version"
	log "golang.aliyun-inc.com/agit/alog/alogcore"
)

var rootCmd = &cobra.Command{
	Use:   "proc-receive-hook",
	Short: "proc-receive-hook is a hook for git to invoke instead of execute commands",
	Long: `proc-receive:

This hook is invoked by linkgit:git-receive-pack[1].  If the server has
set the multi-valued config variable receive.procReceiveRefs, and the
commands sent to 'receive-pack' have matching reference names, these
commands will be executed by this hook, instead of by the internal
execute_commands() function.  This hook is responsible for updating
the relevant references and reporting the results back to 'receive-pack'.

This hook executes once for the receive operation.  It takes no
arguments, but uses a pkt-line format protocol to communicate with
'receive-pack' to read commands, push-options and send results.  In the
following example for the protocol, the letter 'S' stands for
'receive-pack' and the letter 'H' stands for this hook.

    # Version and features negotiation.
    S: PKT-LINE(version=1\0push-options atomic...)
    S: flush-pkt
    H: PKT-LINE(version=1\0push-options...)
    H: flush-pkt

    # Send commands from server to the hook.
    S: PKT-LINE(<old-oid> <new-oid> <ref>)
    S: ... ...
    S: flush-pkt
    # Send push-options only if the 'push-options' feature is enabled.
    S: PKT-LINE(push-option)
    S: ... ...
    S: flush-pkt

    # Receive result from the hook.
    # OK, run this command successfully.
    H: PKT-LINE(ok <ref>)
    # NO, I reject it.
    H: PKT-LINE(ng <ref> <reason>)
    # Fall through, let 'receive-pack' to execute it.
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option fall-through)
    # OK, but has an alternate reference.  The alternate reference name
    # and other status can be given in option directives.
    H: PKT-LINE(ok <ref>)
    H: PKT-LINE(option refname <refname>)
    H: PKT-LINE(option old-oid <old-oid>)
    H: PKT-LINE(option new-oid <new-oid>)
    H: PKT-LINE(option forced-update)
    H: ... ...
    H: flush-pkt

Each command for the 'proc-receive' hook may point to a pseudo-reference
and always has a zero-old as its old-oid, while the 'proc-receive' hook
may update an alternate reference and the alternate reference may exist
already with a non-zero old-oid.  For this case, this hook will use
"option" directives to report extended attributes for the reference given
by the leading "ok" directive.

The report of the commands of this hook should have the same order as
the input.  The exit status of the 'proc-receive' hook only determines
the success or failure of the group of commands sent to it, unless
atomic push is in use.`,
	Version:      version.Version,
	SilenceUsage: true,
	Run: func(cmd *cobra.Command, args []string) {
		if err := run(); err != nil {
			os.Exit(1)
		}
	},
}

func run() error {
	params, err := getSpaceXParamsFromEnv()
	if err != nil {
		return fmt.Errorf("extract spacex params from env failed: %w", err)
	}
	if !params.Enabled {
		return runProcReceiveLegacy()
	}
	return runProcReceive(params)
}

func runProcReceiveLegacy() error {
	span, ctx := tracing.AGitStartSpanFromEnv(context.Background(), "proc-receive-legacy")
	defer span.Finish()

	// turn over all logic to the old proc-receive(proc-receive-legacy)
	cmd := exec.CommandContext(ctx, config.Cfg.LegacyPath)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	cmd.Env = os.Environ()
	if err := cmd.Run(); err != nil {
		ext.Error.Set(span, true)
		span.LogKV("event", "error", "message", err.Error())
		log.WithError(err).Error(ctx, "execute proc-receive-legacy failed")
		return fmt.Errorf("run proc-receive-legacy failed: %w", err)
	}
	return nil
}

func runProcReceive(params *SpaceXParams) error {
	span, ctx := tracing.AGitStartSpanFromEnv(context.Background(), "proc-receive")
	defer span.Finish()

	api := alibaba.NewCodeupAPI(config.Cfg.APIURL, config.Cfg.APPKey, config.Cfg.APPSecret, config.Cfg.ExternUID)

	detector, err := alibaba.NewGlobsDetector(config.Cfg.ProcReceiveRefs, config.Cfg.TagRefs, config.Cfg.HeadsRefs, config.Cfg.MergeRequestsRefs)
	if err != nil {
		return err
	}

	router, err := alibaba.NewCodeupRouter(detector)
	if err != nil {
		return err
	}

	checker := &alibaba.CodeupDivergeChecker{}

	hook := alibaba.NewCodeupHook(os.Stdin, os.Stdout, os.Stderr, api, api, params.MainHooker, config.Cfg.Platform, router, checker)
	if err := hook.Run(ctx); err != nil {
		ext.Error.Set(span, true)
		span.LogKV("event", "error", "message", err.Error())
		log.WithError(err).Error(ctx, "execute proc-receive failed")
		return fmt.Errorf("proc-receive exec failed: %w", err)
	}

	errUpdateRef := voteCommands(ctx, params, hook.GetVotes())
	if errUpdateRef != nil {
		log.WithError(errUpdateRef).Error(ctx, "cluster update ref failed")
	}

	for _, callback := range hook.GetCallbacks() {
		callback(errUpdateRef)
	}

	if err := hook.Report(ctx); err != nil {
		log.WithError(err).Error(ctx, "report hook results failed")
		return fmt.Errorf("[proc-receive] report failed: %w", err)
	}

	return nil
}

// Execute main entrance of proc-receive binary
func Execute() {
	if err := rootCmd.Execute(); err != nil {
		os.Exit(1)
	}
}

func init() {
	cobra.OnInitialize(config.LoadConfig, initALog, initTracing)
}

func initALog() {
	c := config.Cfg.Alog
	var level zapcore.Level
	switch c.Level {
	case "debug":
		level = zapcore.DebugLevel
	case "info":
		level = zapcore.InfoLevel
	case "warn":
		level = zapcore.WarnLevel
	case "error":
		level = zapcore.ErrorLevel
	case "panic":
		level = zapcore.ErrorLevel
	case "fatal":
		level = zapcore.FatalLevel
	default:
		level = zapcore.ErrorLevel
	}
	log.InitLogger(c.FilePath, level, c.MaxSize, c.MaxBackup, c.MaxAge, c.Compress, false, c.ServiceName)
}

func initTracing() {
	tracer, _ := jaeger.NewTracer("hooks", jaeger.NewConstSampler(true), jaeger.NewNullReporter())
	opentracing.SetGlobalTracer(tracer)
}
